Immer 中间件
Immer 是一个独立的 JavaScript 库,它的核心作用是:让你可以用「可变」(mutative)的方式,去编写「不可变」(immutable)的更新逻辑。
在 React 和 Zustand 的世界里,状态更新必须是不可变的。这意味着你不能直接修改状态对象或数组本身,而是必须创建一个新的副本。
Immer 中间件
Immer 中间件是什么?
Immer 是一个独立的 JavaScript 库,它的核心作用是:让你可以用「可变」(mutative)的方式,去编写「不可变」(immutable)的更新逻辑。
在 React 和 Zustand 的世界里,状态更新必须是不可变的。这意味着你不能直接修改状态对象或数组本身,而是必须创建一个新的副本。
没有 Immer 时,更新深层嵌套状态会很麻烦:
// 为了更新 state.user.profile.address.city,你需要这样写:
set((state) => ({
...state,
user: {
...state.user,
profile: {
...state.profile,
address: {
...state.address,
city: 'New York',
},
},
},
}))
使用 Immer 后,你可以像直接修改一样写代码:
set(
produce((state) => {
state.user.profile.address.city = 'New York'
})
)
Immer 会接收你的「可变」操作,在内部应用这些更改到一个临时的草稿(draft)状态上,然后基于草稿为你生成一个全新的、不可变的状态对象。你得到了简单直观的代码,同时保持了不可变性的所有优点。
它的原理是什么?
Immer 的工作原理可以概括为 「写时复制」(Copy-on-Write) 和 「代理」(Proxy)。
创建草稿(Draft):
- 当你调用
produce(baseState, recipe)时,Immer 会接收你的原始状态(baseState)。 - 它不会直接修改
baseState,而是会创建一个该状态的代理对象,这个代理对象就是「草稿」(draft)。
- 当你调用
代理拦截(Interception):
- 这个草稿对象是一个 Proxy。当你像
draft.user.name = 'Alice'这样修改它时,Proxy 会拦截这些「看似可变」的操作。 - 它会在内部记录下所有你对草稿状态的修改路径(例如,「将
user.name的属性值设置为'Alice'」)。
- 这个草稿对象是一个 Proxy。当你像
生成新状态(Patching):
- 一旦你的「修改」函数执行完毕,Immer 会遍历它记录的所有修改。
- 然后,它按需地对原始
baseState进行复制和修改。只有那些被真正修改了的节点才会被创建新的对象,而未被修改的部分则会保持对原状态的引用。
返回结果:
- 最终,Immer 返回一个全新的、包含了所有你所需更改的状态对象。
简单比喻: 就像你有一份重要的纸质文件(原始状态),需要修改几个字。Immer 的做法是:给你一份透明的临摹纸(草稿),你在临摹纸上随便改。改完后,Immer 会看着你的临摹纸,用笔和新的纸(新状态)重新誊写一份,只修改你标出的地方,其他地方照抄。原始文件完好无损。
核心优势:
- 语法简单:像直接修改一样写代码。
- 性能高效:结构共享(Structural Sharing)。只更新变化了的部分,未变化的部分保持引用相等,极大优化了性能和提高内存利用率。
如何在 Zustand 中简单使用?
在 Zustand 中使用 Immer 极其简单,主要有两种方式。
方式一:手动包装 set 函数(推荐,更灵活)
这是最常用和灵活的方式。你不需要安装任何额外的中间件,只需从 immer 包中导入 produce 函数,然后在 set 函数中用它包裹你的更新函数即可。
安装 Immer:
npm install immer在 Store 中使用:
import { create } from 'zustand' import { produce } from 'immer' // 1. 导入 produce 函数 const useStore = create((set) => ({ user: { name: 'Bob', age: 30, address: { city: 'Boston', country: 'USA', }, }, items: [], // 2. 在 set 函数中,用 produce 包裹更新函数 updateAddress: (newCity) => set( produce((state) => { // 现在你可以像直接修改一样写代码了! state.user.address.city = newCity }) ), addItem: (newItem) => set( produce((state) => { state.items.push(newItem) // 直接 push! }) ), updateItemName: (id, newName) => set( produce((state) => { const itemToUpdate = state.items.find((item) => item.id === id) if (itemToUpdate) { itemToUpdate.name = newName // 直接赋值! } }) ), })) export default useStore
方式二:使用 zustand/immer 中间件(自动化)
Zustand 提供了一个官方中间件,可以自动为所有的 set 调用应用 Immer。
从
zustand/immer导入中间件:import { create } from 'zustand' import { immer } from 'zustand/middleware/immer' // 导入中间件 const useStore = create( // 用 immer 中间件包裹你的 store 创建函数 immer((set) => ({ user: { name: 'Bob', age: 30, }, // 现在所有的 set 函数都会自动被 Immer 处理 updateUser: (newName) => set((state) => { state.user.name = newName // 直接修改! // 注意:这里不需要手动调用 produce 了! }), })) )注意:使用此中间件后,
set函数内部的回调函数会自动接收到一个 Immer 的草稿 state,你可以直接修改它。你不再需要也不能手动包裹produce。
总结与选择
个人建议: 对于大多数项目,采用「手动包装 set 函数」的方式更好。因为它让你明确知道在哪里使用了 Immer,代码意图更清晰,并且避免了对所有简单更新(如 set({ count: 1 }))不必要的 Immer 开销。